package com.audio.play;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ShortBuffer;

import android.media.AudioFormat;
import edu.emory.mathcs.jtransforms.fft.FloatFFT_1D;

public class SandBox {
	private static final int NUMBER_OF_CHANNELS = 1;
	private static final int BITS_PER_SAMPLE = 16;
	private static final int BIT_SET_MULTIPLIER = 4;
	private static final int BITS_PER_WORD = 8;
//	private static final int SAMPLE_RATE = 8000; // 44100 if you want a really nice, clean sin wave, but then you must change FFT_SIZE to at least 16384 too
	private static final int SAMPLE_RATE = 44100; // 44100 if you want a really nice, clean sin wave, but then you must change FFT_SIZE to at least 16384 too
	private static final int BYTES_PER_SAMPLE = 2;
	private static final int BASE_FREQUENCY = 110;
	private static final int FRAME_SIZE = 32;
	private static final int SAMPLES_PER_CHARACTER = SAMPLE_RATE / BASE_FREQUENCY * 2;
	private static final double BASE_AMPLITUDE = 4095;
//	private static final int FFT_SIZE = 4096; // 16384 if you use 44100 as the sample rate. FFT happens faster with smaller sizes.
	private static final int FFT_SIZE = 16384; 

	public byte[] modulate(String string) throws  IOException {

		byte[] buffer = new byte[FRAME_SIZE * SAMPLES_PER_CHARACTER * BYTES_PER_SAMPLE];
		ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
		ShortBuffer shortBuffer = byteBuffer.asShortBuffer();

		for (int i = 0; i < string.length() && i < FRAME_SIZE; i++)
		{
			byte byteToModulate = (byte) string.charAt(i);

			modulateByte(shortBuffer, byteToModulate, i % 2 == 0);
		}

		return buffer;
	}

	private void modulateByte(ShortBuffer shortBuffer, byte byteToModulate, boolean even) {
		for (int sampleCount = 0; sampleCount < SAMPLES_PER_CHARACTER; sampleCount++)
		{
			double time = (double) sampleCount / (double) SAMPLE_RATE;
			double sampleValue = 0;
			int oneBits = 0;

			// sum the signals for each bit
			for (int bitNumber = 0; bitNumber < BITS_PER_WORD; bitNumber++)
			{
				boolean bitSet = ((byteToModulate & (byte) (Math.pow(2, bitNumber))) != 0);
				if (bitSet)
				{
					oneBits++;
				}
				sampleValue += Math.sin(2 * Math.PI * BASE_FREQUENCY * (bitNumber + 1) * time) * BASE_AMPLITUDE * (bitSet ? BIT_SET_MULTIPLIER : 1);
			}

			// add in the parity bit
			boolean setParity = ((even && (oneBits % 2 != 0)) || (!even && (oneBits % 2 == 0)));
			sampleValue += Math.sin(2 * Math.PI * BASE_FREQUENCY * (BITS_PER_WORD + 1) * time) * BASE_AMPLITUDE * (setParity ? BIT_SET_MULTIPLIER : 1);

			// average the signals
			sampleValue /= (BITS_PER_WORD + 1);
			shortBuffer.put((short) sampleValue);
		}
	}

	public String demodulateSampleBuffer(ShortBuffer shortBuffer) {
		DemodulatedCharacter lastChar = null;
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < shortBuffer.capacity(); i += SAMPLES_PER_CHARACTER / 2)
		{
			DemodulatedCharacter nextChar = demodulateCharacter(shortBuffer, i, SAMPLES_PER_CHARACTER / 2);
			if (!nextChar.equals(lastChar))
			{
				lastChar = nextChar;
				sb.append(nextChar);
			}
		}

		return sb.toString();
	}

	private DemodulatedCharacter demodulateCharacter(ShortBuffer shortBuffer, int offset, int length) {
		float[] floatArray = new float[FFT_SIZE * 2];
		for (int i = offset; i < shortBuffer.capacity() && i < offset + length; i++)
		{
			floatArray[i - offset] = shortBuffer.get(i);
		}

		FloatFFT_1D fft = new FloatFFT_1D(FFT_SIZE);
		fft.realForward(floatArray);

		int multiplier = (int) (BASE_FREQUENCY / ((float) SAMPLE_RATE / (float) FFT_SIZE));

		long maxPower = findMaxPower(floatArray, multiplier);

		int value = 0;
		for (int i = 0; i < BITS_PER_WORD; i++)
		{
			int index = (i + 1) * multiplier;
			long power = computePowerAtIndex(floatArray, index);
			if (power > (maxPower / (BIT_SET_MULTIPLIER / 2)))
			{
				value += Math.pow(2, i);
			}
		}

		DemodulatedCharacter character = new DemodulatedCharacter();
		character.setData((char) value);

		long parityPower = computePowerAtIndex(floatArray, (BITS_PER_WORD + 1) * multiplier);
		if (parityPower > (maxPower / (BIT_SET_MULTIPLIER / 2)))
		{
			character.setParity(true);
		}

		return character;

	}

	private long findMaxPower(float[] floatArray, int multiplier) {
		long maxPower = 0;
		for (int i = 0; i < BITS_PER_WORD; i++)
		{
			int index = (i + 1) * multiplier;
			long power = computePowerAtIndex(floatArray, index);
			if (power > maxPower)
			{
				maxPower = power;
			}
		}
		return maxPower;
	}

	private long computePowerAtIndex(float[] floatArray, int index) {
		return (long) Math.sqrt(Math.pow(floatArray[index * 2], 2) + Math.pow(floatArray[index * 2 + 1], 2));
	}

	private static class DemodulatedCharacter {
		private char data;
		private boolean parity;

		public char getData() {
			return data;
		}

		public void setData(char data) {
			this.data = data;
		}

		public boolean isParity() {
			return parity;
		}

		public void setParity(boolean parity) {
			this.parity = parity;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + data;
			result = prime * result + (parity ? 1231 : 1237);
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			DemodulatedCharacter other = (DemodulatedCharacter) obj;
			if (data != other.data)
				return false;
			if (parity != other.parity)
				return false;
			return true;
		}

		@Override
		public String toString() {
			return String.valueOf(getData());
		}
	}

}
